﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;

namespace AntsCode.StorageClientExtensions.Helpers
{
    /// <summary>
    /// Converts an expression from one type to another, removing any incompatible expressions.
    /// </summary>
    /// <typeparam name="T">The source type of the expression</typeparam>
    /// <typeparam name="U">The detination type of the expression</typeparam>
    internal class ExpressionConverter<T, U> : IExpressionConverter
    {
        public Expression ConvertExpression(Expression sourceExpression)
        {
            LambdaExpression exp = (LambdaExpression)sourceExpression;

            // Begin the conversion
            Expression converted = ConvertExpressionPart(exp.Body);

            // Get any parameters from the converted expression for use in the lambda expression
            // This is because the references must match to prevent an "out of scope" exception
            ParameterExpression[] parameters = GetParameters(converted, new List<ParameterExpression>()).ToArray();

            if (parameters.Length == 0)
            {
                // This can happen if the expression does not use a parameter of the destination type
                // However we still need to add the parameter to the lambda for it to return the correct type
                parameters = new ParameterExpression[] { Expression.Parameter(typeof(U), "t") };
            }

            return Expression.Lambda(converted, parameters);
        }

        private Expression ConvertExpressionPart(Expression sourceExp)
        {
            Expression destExp;
            Type sourceExpType = sourceExp.GetType();

            if (sourceExpType == typeof(MemberExpression))
            {
                destExp = ConvertMemberExpression((MemberExpression)sourceExp);
            }
            else if (sourceExpType == typeof(ConstantExpression))
            {
                destExp = ConvertConstantExpression((ConstantExpression)sourceExp);
            }
            else if (sourceExpType == typeof(MethodCallExpression))
            {
                destExp = ConvertMethodCallExpression((MethodCallExpression)sourceExp);   
            }
            else if (sourceExpType == typeof(BinaryExpression))
            {
                destExp = ConvertBinaryExpression((BinaryExpression)sourceExp);
            }
            else if (sourceExpType == typeof(NewExpression))
            {
                destExp = ConvertNewExpression((NewExpression)sourceExp);
            }
            else if (sourceExpType == typeof(ConditionalExpression))
            {
                destExp = ConvertConditionalExpression((ConditionalExpression)sourceExp);
            }
            else if (sourceExpType == typeof(UnaryExpression))
            {
                destExp = ConvertUnaryExpression((UnaryExpression)sourceExp);
            }
            else
            {
                destExp = sourceExp;
            }

            return destExp;
        }

        private ConstantExpression ConvertConstantExpression(ConstantExpression sourceExp)
        {
            ConstantExpression sourceConstExp = (ConstantExpression)sourceExp;
            object value;

            if (sourceConstExp.Value == null)
            {
                value = null;
            }
            else
            {
                value = sourceConstExp.Value;
            }

            return Expression.Constant(value);
        }

        private MethodCallExpression ConvertMethodCallExpression(MethodCallExpression sourceExp)
        {
            MethodCallExpression destExp;


            Expression destObj = null;
            if (sourceExp.Object != null)
            {
                destObj = ConvertExpressionPart(sourceExp.Object);
            }

            List<Expression> destArgs = new List<Expression>();
            foreach (Expression arg in sourceExp.Arguments)
            {
                destArgs.Add(ConvertExpressionPart(arg));
            }

            destExp = Expression.Call(destObj, sourceExp.Method, destArgs);

            return destExp;
        }

        private Expression ConvertMemberExpression(MemberExpression sourceExp)
        {
            Expression destExp = null;

            if (sourceExp.Expression != null)
            {
                if (sourceExp.Expression.NodeType == ExpressionType.Constant)
                {
                    destExp = sourceExp;
                }
                else
                {
                    if (sourceExp.Expression.Type == typeof(U))
                    {
                        // Types are the same
                        ParameterExpression parameter = Expression.Parameter(typeof(U), "t");
                        destExp = Expression.MakeMemberAccess(parameter, sourceExp.Member);
                    }
                    else
                    {
                        // Is the destination type an anonyous type that includes the source type?
                        MemberInfo foundMember = typeof(U).GetProperties().FirstOrDefault(p => p.PropertyType == sourceExp.Expression.Type);

                        if (foundMember != null)
                        {
                            // Yes, so build a nested member expression
                            ParameterExpression parameter = Expression.Parameter(typeof(U), "t");

                            destExp = Expression.MakeMemberAccess(parameter, foundMember);
                            destExp = Expression.MakeMemberAccess(destExp, sourceExp.Member);
                        }
                    }
                }
            }
            else
            {
                destExp = sourceExp;
            }

            return destExp;
        }

        private NewExpression ConvertNewExpression(NewExpression newExp)
        {
            List<Expression> newArgs = new List<Expression>();
            Expression convertedArg;

            foreach (Expression arg in newExp.Arguments)
            {
                convertedArg = ConvertExpressionPart(arg);

                if (convertedArg != null)
                {
                    newArgs.Add(convertedArg);
                }
            }

            var ci = typeof(T).GetConstructors()[0];

            return Expression.New(ci, newArgs);
        }

        private BinaryExpression ConvertBinaryExpression(BinaryExpression source)
        {
            BinaryExpression converted = null;

            // Convert the left & right parts
            Expression convertedLeft = ConvertExpressionPart(source.Left);
            Expression convertedRight = ConvertExpressionPart(source.Right);

            // If either the left part or right part is null, then the expression could not be converted
            // (i.e. it contains a type other than the destination type
            if (convertedLeft != null && convertedRight != null)
            {
                // Create a new binary expression 
                converted = Expression.MakeBinary(source.NodeType, convertedLeft, convertedRight, source.IsLiftedToNull, source.Method);
            }

            return converted;
        }

        private ConditionalExpression ConvertConditionalExpression(ConditionalExpression source)
        {
            ConditionalExpression converted = null;

            // Convert the true, false & test parts
            Expression convertedTrue = ConvertExpressionPart(source.IfTrue);
            Expression convertedFalse = ConvertExpressionPart(source.IfFalse);
            Expression convertedTest = ConvertExpressionPart(source.Test);

            // If any part is null, then the expression could not be converted
            // (i.e. it contains a type other than the destination type
            if (convertedTrue != null && convertedFalse != null && convertedTest != null)
            {
                // Create a new binary expression 
                converted = Expression.Condition(convertedTest, convertedTrue, convertedFalse);
            }

            return converted;
        }

        private UnaryExpression ConvertUnaryExpression(UnaryExpression source)
        {
            UnaryExpression converted = null;

            // Convert the operand
            Expression convertedOperand = ConvertExpressionPart(source.Operand);

            if (convertedOperand != null)
            {
                if (source.NodeType == ExpressionType.Convert)
                {
                    converted = Expression.Convert(convertedOperand, source.Type);
                }
            }

            return converted;
        }

        public List<ParameterExpression> GetParameters(Expression exp, List<ParameterExpression> parameters)
        {
            if (exp != null)
            {
                Type expType = exp.GetType();

                if (expType == typeof(NewExpression))
                {
                    NewExpression newExp = (NewExpression)exp;

                    // Look for parameters in each argument
                    foreach (Expression argument in newExp.Arguments)
                    {
                        parameters = GetParameters(argument, parameters);
                    }
                }
                else if (expType == typeof(MemberExpression))
                {
                    MemberExpression memExp = (MemberExpression)exp;

                    // Look for parameters in the member expression
                    parameters = GetParameters(memExp.Expression, parameters);
                }
                else if (expType == typeof(MethodCallExpression))
                {
                    MethodCallExpression methCallExp = (MethodCallExpression)exp;

                    // Look for parameters in the method call object
                    if (methCallExp.Object != null)
                    {
                        parameters = GetParameters(methCallExp.Object, parameters);
                    }

                    // Look for parameters in each method argument
                    foreach (Expression argument in methCallExp.Arguments)
                    {
                        parameters = GetParameters(argument, parameters);
                    }
                }
                else if (expType == typeof(BinaryExpression))
                {
                    // Look for parameters in each binary part
                    BinaryExpression binExp = (BinaryExpression)exp;

                    parameters = GetParameters(binExp.Left, parameters);
                    parameters = GetParameters(binExp.Right, parameters);
                }
                else if (expType == typeof(ParameterExpression))
                {
                    ParameterExpression paramExp = (ParameterExpression)exp;

                    // Does a parameter of this type already exist?
                    if (parameters.FirstOrDefault(p => p.Type == paramExp.Type) == null)
                    {
                        // No, so add the parameter to the list
                        parameters.Add(paramExp);
                    }
                }
                else if (expType == typeof(ConditionalExpression))
                {
                    ConditionalExpression conditionalExp = (ConditionalExpression)exp;

                    parameters = GetParameters(conditionalExp.IfFalse, parameters);
                    parameters = GetParameters(conditionalExp.IfTrue, parameters);
                    parameters = GetParameters(conditionalExp.Test, parameters);
                }
                else if (expType == typeof(UnaryExpression))
                {
                    UnaryExpression unaryExp = (UnaryExpression)exp;

                    parameters = GetParameters(unaryExp.Operand, parameters);
                }
            }

            return parameters;
        }
    }
}